Jelajahi tipe bermerek TypeScript, teknik canggih untuk mencapai pengetikan nominal dalam sistem tipe struktural. Pelajari cara meningkatkan keamanan tipe dan kejelasan kode.
Tipe Bermerek TypeScript: Pengetikan Nominal dalam Sistem Struktural
Sistem tipe struktural TypeScript menawarkan fleksibilitas tetapi terkadang dapat menyebabkan perilaku yang tidak terduga. Tipe bermerek (branded types) menyediakan cara untuk memberlakukan pengetikan nominal, meningkatkan keamanan tipe dan kejelasan kode. Artikel ini menjelajahi tipe bermerek secara mendetail, memberikan contoh praktis dan praktik terbaik untuk implementasinya.
Memahami Pengetikan Struktural vs. Nominal
Sebelum mendalami tipe bermerek, mari kita perjelas perbedaan antara pengetikan struktural dan nominal.
Pengetikan Struktural (Duck Typing)
Dalam sistem tipe struktural, dua tipe dianggap kompatibel jika mereka memiliki struktur yang sama (yaitu, properti yang sama dengan tipe yang sama). TypeScript menggunakan pengetikan struktural. Perhatikan contoh ini:
interface Point {
x: number;
y: number;
}
interface Vector {
x: number;
y: number;
}
const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // Valid in TypeScript
console.log(vector.x); // Output: 10
Meskipun Point
dan Vector
dideklarasikan sebagai tipe yang berbeda, TypeScript mengizinkan penugasan objek Point
ke variabel Vector
karena keduanya berbagi struktur yang sama. Ini bisa jadi nyaman, tetapi juga dapat menyebabkan kesalahan jika Anda perlu membedakan antara tipe yang secara logis berbeda namun kebetulan memiliki bentuk yang sama. Sebagai contoh, bayangkan koordinat untuk lintang/bujur yang mungkin secara kebetulan cocok dengan koordinat piksel layar.
Pengetikan Nominal
Dalam sistem tipe nominal, tipe dianggap kompatibel hanya jika mereka memiliki nama yang sama. Bahkan jika dua tipe memiliki struktur yang sama, mereka diperlakukan sebagai tipe yang berbeda jika memiliki nama yang berbeda. Bahasa seperti Java dan C# menggunakan pengetikan nominal.
Kebutuhan akan Tipe Bermerek
Pengetikan struktural TypeScript bisa menjadi masalah ketika Anda perlu memastikan bahwa sebuah nilai milik tipe tertentu, terlepas dari strukturnya. Sebagai contoh, pertimbangkan representasi mata uang. Anda mungkin memiliki tipe yang berbeda untuk USD dan EUR, tetapi keduanya bisa direpresentasikan sebagai angka. Tanpa mekanisme untuk membedakannya, Anda bisa secara tidak sengaja melakukan operasi pada mata uang yang salah.
Tipe bermerek mengatasi masalah ini dengan memungkinkan Anda membuat tipe-tipe berbeda yang secara struktural serupa tetapi diperlakukan sebagai tipe yang berbeda oleh sistem tipe. Ini meningkatkan keamanan tipe dan mencegah kesalahan yang mungkin lolos begitu saja.
Mengimplementasikan Tipe Bermerek di TypeScript
Tipe bermerek diimplementasikan menggunakan tipe persimpangan (intersection types) dan simbol unik atau literal string. Idenya adalah menambahkan "merek" ke sebuah tipe yang membedakannya dari tipe lain dengan struktur yang sama.
Menggunakan Simbol (Disarankan)
Menggunakan simbol untuk penandaan merek umumnya lebih disukai karena simbol dijamin unik.
const USD = Symbol('USD');
type USD = number & { readonly [USD]: unique symbol };
const EUR = Symbol('EUR');
type EUR = number & { readonly [EUR]: unique symbol };
function createUSD(value: number): USD {
return value as USD;
}
function createEUR(value: number): EUR {
return value as EUR;
}
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);
const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);
// Uncommenting the next line will cause a type error
// const invalidOperation = addUSD(usd1, eur1);
Dalam contoh ini, USD
dan EUR
adalah tipe bermerek yang didasarkan pada tipe number
. unique symbol
memastikan bahwa tipe-tipe ini berbeda. Fungsi createUSD
dan createEUR
digunakan untuk membuat nilai dari tipe-tipe ini, dan fungsi addUSD
hanya menerima nilai USD
. Mencoba menambahkan nilai EUR
ke nilai USD
akan menghasilkan kesalahan tipe.
Menggunakan Literal String
Anda juga dapat menggunakan literal string untuk penandaan merek, meskipun pendekatan ini kurang kuat dibandingkan menggunakan simbol karena literal string tidak dijamin unik.
type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };
function createUSD(value: number): USD {
return value as USD;
}
function createEUR(value: number): EUR {
return value as EUR;
}
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);
const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);
// Uncommenting the next line will cause a type error
// const invalidOperation = addUSD(usd1, eur1);
Contoh ini mencapai hasil yang sama seperti contoh sebelumnya, tetapi menggunakan literal string alih-alih simbol. Meskipun lebih sederhana, penting untuk memastikan bahwa literal string yang digunakan untuk penandaan merek bersifat unik di dalam basis kode Anda.
Contoh Praktis dan Kasus Penggunaan
Tipe bermerek dapat diterapkan pada berbagai skenario di mana Anda perlu memberlakukan keamanan tipe di luar kompatibilitas struktural.
ID
Pertimbangkan sebuah sistem dengan berbagai jenis ID, seperti UserID
, ProductID
, dan OrderID
. Semua ID ini mungkin direpresentasikan sebagai angka atau string, tetapi Anda ingin mencegah pencampuran tipe ID yang berbeda secara tidak sengaja.
const UserIDBrand = Symbol('UserID');
type UserID = string & { readonly [UserIDBrand]: unique symbol };
const ProductIDBrand = Symbol('ProductID');
type ProductID = string & { readonly [ProductIDBrand]: unique symbol };
function getUser(id: UserID): { name: string } {
// ... fetch user data
return { name: "Alice" };
}
function getProduct(id: ProductID): { name: string, price: number } {
// ... fetch product data
return { name: "Example Product", price: 25 };
}
function createUserID(id: string): UserID {
return id as UserID;
}
function createProductID(id: string): ProductID {
return id as ProductID;
}
const userID = createUserID('user123');
const productID = createProductID('product456');
const user = getUser(userID);
const product = getProduct(productID);
console.log("User:", user);
console.log("Product:", product);
// Uncommenting the next line will cause a type error
// const invalidCall = getUser(productID);
Contoh ini menunjukkan bagaimana tipe bermerek dapat mencegah pengiriman ProductID
ke fungsi yang mengharapkan UserID
, sehingga meningkatkan keamanan tipe.
Nilai Spesifik Domain
Tipe bermerek juga dapat berguna untuk merepresentasikan nilai-nilai spesifik domain dengan batasan. Misalnya, Anda mungkin memiliki tipe untuk persentase yang harus selalu berada di antara 0 dan 100.
const PercentageBrand = Symbol('Percentage');
type Percentage = number & { readonly [PercentageBrand]: unique symbol };
function createPercentage(value: number): Percentage {
if (value < 0 || value > 100) {
throw new Error('Percentage must be between 0 and 100');
}
return value as Percentage;
}
function applyDiscount(price: number, discount: Percentage): number {
return price * (1 - discount / 100);
}
try {
const discount = createPercentage(20);
const discountedPrice = applyDiscount(100, discount);
console.log("Discounted Price:", discountedPrice);
// Uncommenting the next line will cause an error during runtime
// const invalidPercentage = createPercentage(120);
} catch (error) {
console.error(error);
}
Contoh ini menunjukkan cara memberlakukan batasan pada nilai tipe bermerek saat runtime. Meskipun sistem tipe tidak dapat menjamin bahwa nilai Percentage
selalu antara 0 dan 100, fungsi createPercentage
dapat memberlakukan batasan ini saat runtime. Anda juga dapat menggunakan pustaka seperti io-ts untuk memberlakukan validasi runtime pada tipe bermerek.
Representasi Tanggal dan Waktu
Bekerja dengan tanggal dan waktu bisa rumit karena berbagai format dan zona waktu. Tipe bermerek dapat membantu membedakan antara representasi tanggal dan waktu yang berbeda.
const UTCDateBrand = Symbol('UTCDate');
type UTCDate = string & { readonly [UTCDateBrand]: unique symbol };
const LocalDateBrand = Symbol('LocalDate');
type LocalDate = string & { readonly [LocalDateBrand]: unique symbol };
function createUTCDate(dateString: string): UTCDate {
// Validate that the date string is in UTC format (e.g., ISO 8601 with Z)
if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
throw new Error('Invalid UTC date format');
}
return dateString as UTCDate;
}
function createLocalDate(dateString: string): LocalDate {
// Validate that the date string is in local date format (e.g., YYYY-MM-DD)
if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
throw new Error('Invalid local date format');
}
return dateString as LocalDate;
}
function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
// Perform time zone conversion
const date = new Date(utcDate);
const localDateString = date.toLocaleDateString();
return createLocalDate(localDateString);
}
try {
const utcDate = createUTCDate('2024-01-20T10:00:00.000Z');
const localDate = convertUTCDateToLocalDate(utcDate);
console.log("UTC Date:", utcDate);
console.log("Local Date:", localDate);
} catch (error) {
console.error(error);
}
Contoh ini membedakan antara tanggal UTC dan lokal, memastikan bahwa Anda bekerja dengan representasi tanggal dan waktu yang benar di berbagai bagian aplikasi Anda. Validasi runtime memastikan bahwa hanya string tanggal yang diformat dengan benar yang dapat diberi tipe ini.
Praktik Terbaik untuk Menggunakan Tipe Bermerek
Untuk menggunakan tipe bermerek secara efektif di TypeScript, pertimbangkan praktik terbaik berikut:
- Gunakan Simbol untuk Penandaan Merek: Simbol memberikan jaminan keunikan terkuat, mengurangi risiko kesalahan tipe.
- Buat Fungsi Bantuan: Gunakan fungsi bantuan untuk membuat nilai dari tipe bermerek. Ini menyediakan titik pusat untuk validasi dan memastikan konsistensi.
- Terapkan Validasi Runtime: Meskipun tipe bermerek meningkatkan keamanan tipe, mereka tidak mencegah nilai yang salah ditetapkan saat runtime. Gunakan validasi runtime untuk memberlakukan batasan.
- Dokumentasikan Tipe Bermerek: Dokumentasikan dengan jelas tujuan dan batasan dari setiap tipe bermerek untuk meningkatkan kemudahan pemeliharaan kode.
- Pertimbangkan Implikasi Kinerja: Tipe bermerek memperkenalkan overhead kecil karena tipe persimpangan dan kebutuhan akan fungsi bantuan. Pertimbangkan dampak kinerja di bagian kode Anda yang kritis terhadap kinerja.
Kelebihan Tipe Bermerek
- Keamanan Tipe yang Ditingkatkan: Mencegah pencampuran yang tidak disengaja dari tipe yang secara struktural serupa tetapi secara logis berbeda.
- Kejelasan Kode yang Ditingkatkan: Membuat kode lebih mudah dibaca dan dipahami dengan membedakan tipe secara eksplisit.
- Mengurangi Kesalahan: Menangkap potensi kesalahan pada saat kompilasi, mengurangi risiko bug saat runtime.
- Kemudahan Pemeliharaan yang Meningkat: Membuat kode lebih mudah dipelihara dan direfaktor dengan menyediakan pemisahan masalah yang jelas.
Kekurangan Tipe Bermerek
- Kompleksitas yang Meningkat: Menambahkan kompleksitas pada basis kode, terutama saat berhadapan dengan banyak tipe bermerek.
- Overhead Runtime: Memperkenalkan overhead runtime kecil karena kebutuhan akan fungsi bantuan dan validasi runtime.
- Potensi Kode Boilerplate: Dapat menyebabkan kode boilerplate, terutama saat membuat dan memvalidasi tipe bermerek.
Alternatif untuk Tipe Bermerek
Meskipun tipe bermerek adalah teknik yang kuat untuk mencapai pengetikan nominal di TypeScript, ada pendekatan alternatif yang mungkin Anda pertimbangkan.
Tipe Opak (Opaque Types)
Tipe opak mirip dengan tipe bermerek tetapi menyediakan cara yang lebih eksplisit untuk menyembunyikan tipe yang mendasarinya. TypeScript tidak memiliki dukungan bawaan untuk tipe opak, tetapi Anda dapat mensimulasikannya menggunakan modul dan simbol privat.
Kelas
Menggunakan kelas dapat memberikan pendekatan yang lebih berorientasi objek untuk mendefinisikan tipe yang berbeda. Meskipun kelas diketik secara struktural di TypeScript, mereka menawarkan pemisahan masalah yang lebih jelas dan dapat digunakan untuk memberlakukan batasan melalui metode.
Pustaka seperti `io-ts` atau `zod`
Pustaka-pustaka ini menyediakan validasi tipe runtime yang canggih dan dapat digabungkan dengan tipe bermerek untuk memastikan keamanan baik pada saat kompilasi maupun saat runtime.
Kesimpulan
Tipe bermerek TypeScript adalah alat yang berharga untuk meningkatkan keamanan tipe dan kejelasan kode dalam sistem tipe struktural. Dengan menambahkan "merek" pada sebuah tipe, Anda dapat memberlakukan pengetikan nominal dan mencegah pencampuran yang tidak disengaja dari tipe yang secara struktural serupa tetapi secara logis berbeda. Meskipun tipe bermerek memperkenalkan beberapa kompleksitas dan overhead, manfaat dari peningkatan keamanan tipe dan kemudahan pemeliharaan kode sering kali lebih besar daripada kekurangannya. Pertimbangkan untuk menggunakan tipe bermerek dalam skenario di mana Anda perlu memastikan bahwa sebuah nilai milik tipe tertentu, terlepas dari strukturnya.
Dengan memahami prinsip-prinsip di balik pengetikan struktural dan nominal, dan dengan menerapkan praktik terbaik yang diuraikan dalam artikel ini, Anda dapat secara efektif memanfaatkan tipe bermerek untuk menulis kode TypeScript yang lebih kuat dan mudah dipelihara. Dari merepresentasikan mata uang dan ID hingga memberlakukan batasan spesifik domain, tipe bermerek menyediakan mekanisme yang fleksibel dan kuat untuk meningkatkan keamanan tipe dalam proyek Anda.
Saat Anda bekerja dengan TypeScript, jelajahi berbagai teknik dan pustaka yang tersedia untuk validasi dan penegakan tipe. Pertimbangkan untuk menggunakan tipe bermerek bersama dengan pustaka validasi runtime seperti io-ts
atau zod
untuk mencapai pendekatan yang komprehensif terhadap keamanan tipe.